Buka potensi multithreading sejati di JavaScript. Panduan komprehensif ini membahas SharedArrayBuffer, Atomics, Web Worker, dan persyaratan keamanan untuk aplikasi web performa tinggi.
JavaScript SharedArrayBuffer: Menyelami Pemrograman Konkuren di Web Secara Mendalam
Selama beberapa dekade, sifat single-threaded JavaScript telah menjadi sumber kesederhanaannya sekaligus hambatan performa yang signifikan. Model event loop bekerja dengan sangat baik untuk sebagian besar tugas yang digerakkan oleh UI, tetapi kesulitan saat dihadapkan pada operasi yang intensif secara komputasi. Kalkulasi yang berjalan lama dapat membekukan browser, menciptakan pengalaman pengguna yang membuat frustrasi. Meskipun Web Worker menawarkan solusi parsial dengan memungkinkan skrip berjalan di latar belakang, mereka datang dengan batasan utama mereka sendiri: komunikasi data yang tidak efisien.
Masuklah SharedArrayBuffer
(SAB), sebuah fitur canggih yang secara fundamental mengubah permainan dengan memperkenalkan berbagi memori tingkat rendah yang sebenarnya antar-thread di web. Dipasangkan dengan objek Atomics
, SAB membuka era baru aplikasi berkinerja tinggi dan konkuren langsung di browser. Namun, dengan kekuatan besar datang pula tanggung jawab besar—dan kompleksitas.
Panduan ini akan membawa Anda menyelami dunia pemrograman konkuren di JavaScript. Kita akan menjelajahi mengapa kita membutuhkannya, cara kerja SharedArrayBuffer
dan Atomics
, pertimbangan keamanan kritis yang harus Anda atasi, dan contoh praktis untuk memulai.
Dunia Lama: Model Single-Threaded JavaScript dan Keterbatasannya
Sebelum kita dapat menghargai solusinya, kita harus sepenuhnya memahami masalahnya. Eksekusi JavaScript di browser secara tradisional terjadi pada satu thread, sering disebut "main thread" atau "UI thread".
Event Loop
Main thread bertanggung jawab untuk segalanya: mengeksekusi kode JavaScript Anda, merender halaman, merespons interaksi pengguna (seperti klik dan scroll), dan menjalankan animasi CSS. Ia mengelola tugas-tugas ini menggunakan event loop, yang secara terus-menerus memproses antrean pesan (tugas). Jika sebuah tugas memakan waktu lama untuk selesai, ia akan memblokir seluruh antrean. Tidak ada hal lain yang bisa terjadi—UI membeku, animasi tersendat, dan halaman menjadi tidak responsif.
Web Worker: Sebuah Langkah ke Arah yang Benar
Web Worker diperkenalkan untuk mengatasi masalah ini. Sebuah Web Worker pada dasarnya adalah skrip yang berjalan di thread latar belakang yang terpisah. Anda dapat memindahkan komputasi berat ke worker, menjaga main thread tetap bebas untuk menangani antarmuka pengguna.
Komunikasi antara main thread dan worker terjadi melalui API postMessage()
. Ketika Anda mengirim data, data tersebut ditangani oleh algoritma structured clone. Ini berarti data diserialisasi, disalin, dan kemudian dideserialisasi dalam konteks worker. Meskipun efektif, proses ini memiliki kelemahan signifikan untuk dataset besar:
- Beban Performa: Menyalin data berukuran megabyte atau bahkan gigabyte antar-thread itu lambat dan intensif CPU.
- Konsumsi Memori: Proses ini menciptakan duplikat data di memori, yang bisa menjadi masalah besar untuk perangkat dengan memori terbatas.
Bayangkan sebuah editor video di browser. Mengirim seluruh frame video (yang bisa berukuran beberapa megabyte) bolak-balik ke worker untuk diproses 60 kali per detik akan sangat mahal. Inilah masalah yang dirancang untuk dipecahkan oleh SharedArrayBuffer
.
Sang Pengubah Permainan: Memperkenalkan SharedArrayBuffer
Sebuah SharedArrayBuffer
adalah buffer data biner mentah dengan panjang tetap, mirip dengan ArrayBuffer
. Perbedaan kritisnya adalah SharedArrayBuffer
dapat dibagikan di beberapa thread (misalnya, main thread dan satu atau lebih Web Worker). Ketika Anda "mengirim" SharedArrayBuffer
menggunakan postMessage()
, Anda tidak mengirim salinan; Anda mengirim referensi ke blok memori yang sama.
Ini berarti setiap perubahan yang dibuat pada data buffer oleh satu thread akan langsung terlihat oleh semua thread lain yang memiliki referensi ke sana. Ini menghilangkan langkah salin-dan-serialisasi yang mahal, memungkinkan berbagi data yang hampir seketika.
Anggap saja seperti ini:
- Web Worker dengan
postMessage()
: Ini seperti dua kolega yang mengerjakan sebuah dokumen dengan saling mengirim salinan melalui email. Setiap perubahan memerlukan pengiriman salinan baru secara keseluruhan. - Web Worker dengan
SharedArrayBuffer
: Ini seperti dua kolega yang mengerjakan dokumen yang sama di editor online bersama (seperti Google Docs). Perubahan terlihat oleh keduanya secara real-time.
Bahaya Memori Bersama: Kondisi Balapan (Race Conditions)
Berbagi memori secara instan memang kuat, tetapi juga memperkenalkan masalah klasik dari dunia pemrograman konkuren: kondisi balapan (race conditions).
Kondisi balapan terjadi ketika beberapa thread mencoba mengakses dan memodifikasi data bersama yang sama secara bersamaan, dan hasil akhirnya bergantung pada urutan eksekusi mereka yang tidak dapat diprediksi. Pertimbangkan sebuah penghitung sederhana yang disimpan dalam SharedArrayBuffer
. Baik main thread maupun worker ingin menaikkan nilainya.
- Thread A membaca nilai saat ini, yaitu 5.
- Sebelum Thread A dapat menulis nilai baru, sistem operasi menjedanya dan beralih ke Thread B.
- Thread B membaca nilai saat ini, yang masih 5.
- Thread B menghitung nilai baru (6) dan menuliskannya kembali ke memori.
- Sistem beralih kembali ke Thread A. Ia tidak tahu bahwa Thread B melakukan apa pun. Ia melanjutkan dari tempat ia berhenti, menghitung nilai barunya (5 + 1 = 6) dan menulis 6 kembali ke memori.
Meskipun penghitung dinaikkan dua kali, nilai akhirnya adalah 6, bukan 7. Operasi tersebut tidak atomik—mereka dapat diinterupsi, yang menyebabkan hilangnya data. Inilah alasan mengapa Anda tidak dapat menggunakan SharedArrayBuffer
tanpa mitra krusialnya: objek Atomics
.
Sang Penjaga Memori Bersama: Objek Atomics
Objek Atomics
menyediakan serangkaian metode statis untuk melakukan operasi atomik pada objek SharedArrayBuffer
. Sebuah operasi atomik dijamin akan dilakukan secara keseluruhan tanpa diinterupsi oleh operasi lain. Entah itu terjadi sepenuhnya atau tidak sama sekali.
Menggunakan Atomics
mencegah kondisi balapan dengan memastikan bahwa operasi baca-modifikasi-tulis pada memori bersama dilakukan dengan aman.
Metode Kunci Atomics
Mari kita lihat beberapa metode paling penting yang disediakan oleh Atomics
.
Atomics.load(typedArray, index)
: Secara atomik membaca nilai pada indeks tertentu dan mengembalikannya. Ini memastikan Anda membaca nilai yang lengkap dan tidak rusak.Atomics.store(typedArray, index, value)
: Secara atomik menyimpan nilai pada indeks tertentu dan mengembalikan nilai tersebut. Ini memastikan operasi tulis tidak terganggu.Atomics.add(typedArray, index, value)
: Secara atomik menambahkan sebuah nilai ke nilai pada indeks yang diberikan. Ia mengembalikan nilai asli pada posisi tersebut. Ini adalah padanan atomik darix += value
.Atomics.sub(typedArray, index, value)
: Secara atomik mengurangi sebuah nilai dari nilai pada indeks yang diberikan.Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
: Ini adalah operasi tulis kondisional yang kuat. Ia memeriksa apakah nilai padaindex
sama denganexpectedValue
. Jika ya, ia menggantinya denganreplacementValue
dan mengembalikanexpectedValue
asli. Jika tidak, ia tidak melakukan apa-apa dan mengembalikan nilai saat ini. Ini adalah blok bangunan fundamental untuk mengimplementasikan primitif sinkronisasi yang lebih kompleks seperti kunci (locks).
Sinkronisasi: Melampaui Operasi Sederhana
Terkadang Anda membutuhkan lebih dari sekadar membaca dan menulis yang aman. Anda perlu agar thread dapat berkoordinasi dan menunggu satu sama lain. Anti-pola yang umum adalah "busy-waiting," di mana sebuah thread berada dalam loop ketat, terus-menerus memeriksa lokasi memori untuk perubahan. Ini membuang-buang siklus CPU dan menguras masa pakai baterai.
Atomics
menyediakan solusi yang jauh lebih efisien dengan wait()
dan notify()
.
Atomics.wait(typedArray, index, value, timeout)
: Ini memberitahu sebuah thread untuk tidur. Ia memeriksa apakah nilai padaindex
masihvalue
. Jika ya, thread tersebut tidur sampai dibangunkan olehAtomics.notify()
atau sampaitimeout
opsional (dalam milidetik) tercapai. Jika nilai padaindex
sudah berubah, ia akan segera kembali. Ini sangat efisien karena thread yang tidur hampir tidak mengonsumsi sumber daya CPU.Atomics.notify(typedArray, index, count)
: Ini digunakan untuk membangunkan thread yang sedang tidur di lokasi memori tertentu melaluiAtomics.wait()
. Ia akan membangunkan paling banyakcount
thread yang menunggu (atau semuanya jikacount
tidak disediakan atau bernilaiInfinity
).
Menyatukan Semuanya: Panduan Praktis
Sekarang setelah kita memahami teorinya, mari kita ikuti langkah-langkah mengimplementasikan solusi menggunakan SharedArrayBuffer
.
Langkah 1: Prasyarat Keamanan - Isolasi Lintas-Asal (Cross-Origin Isolation)
Ini adalah batu sandungan paling umum bagi para pengembang. Untuk alasan keamanan, SharedArrayBuffer
hanya tersedia di halaman yang berada dalam status terisolasi lintas-asal (cross-origin isolated). Ini adalah tindakan keamanan untuk mengurangi kerentanan eksekusi spekulatif seperti Spectre, yang berpotensi menggunakan timer beresolusi tinggi (yang dimungkinkan oleh memori bersama) untuk membocorkan data antar-asal.
Untuk mengaktifkan isolasi lintas-asal, Anda harus mengonfigurasi server web Anda untuk mengirim dua header HTTP spesifik untuk dokumen utama Anda:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
(COOP): Mengisolasi konteks penjelajahan dokumen Anda dari dokumen lain, mencegah mereka berinteraksi langsung dengan objek window Anda.Cross-Origin-Embedder-Policy: require-corp
(COEP): Mengharuskan semua sub-sumber daya (seperti gambar, skrip, dan iframe) yang dimuat oleh halaman Anda harus berasal dari asal yang sama atau secara eksplisit ditandai dapat dimuat lintas-asal dengan headerCross-Origin-Resource-Policy
atau CORS.
Ini bisa menjadi tantangan untuk diatur, terutama jika Anda bergantung pada skrip atau sumber daya pihak ketiga yang tidak menyediakan header yang diperlukan. Setelah mengonfigurasi server Anda, Anda dapat memverifikasi apakah halaman Anda terisolasi dengan memeriksa properti self.crossOriginIsolated
di konsol browser. Nilainya harus true
.
Langkah 2: Membuat dan Berbagi Buffer
Di skrip utama Anda, Anda membuat SharedArrayBuffer
dan "tampilan" (view) di atasnya menggunakan TypedArray
seperti Int32Array
.
main.js:
// Cek isolasi lintas-asal terlebih dahulu!
if (!self.crossOriginIsolated) {
console.error("Halaman ini tidak terisolasi lintas-asal. SharedArrayBuffer tidak akan tersedia.");
} else {
// Buat buffer bersama untuk satu integer 32-bit.
const buffer = new SharedArrayBuffer(4);
// Buat tampilan (view) di atas buffer. Semua operasi atomik terjadi pada tampilan ini.
const int32Array = new Int32Array(buffer);
// Inisialisasi nilai pada indeks 0.
int32Array[0] = 0;
// Buat worker baru.
const worker = new Worker('worker.js');
// Kirim buffer BERSAMA ke worker. Ini adalah transfer referensi, bukan salinan.
worker.postMessage({ buffer });
// Dengarkan pesan dari worker.
worker.onmessage = (event) => {
console.log(`Worker melaporkan selesai. Nilai akhir: ${Atomics.load(int32Array, 0)}`);
};
}
Langkah 3: Melakukan Operasi Atomik di Worker
Worker menerima buffer dan sekarang dapat melakukan operasi atomik padanya.
worker.js:
self.onmessage = (event) => {
const { buffer } = event.data;
const int32Array = new Int32Array(buffer);
console.log("Worker menerima buffer bersama.");
// Mari lakukan beberapa operasi atomik.
for (let i = 0; i < 1000000; i++) {
// Naikkan nilai bersama dengan aman.
Atomics.add(int32Array, 0, 1);
}
console.log("Worker selesai menaikkan nilai.");
// Beri sinyal kembali ke main thread bahwa kita sudah selesai.
self.postMessage({ done: true });
};
Langkah 4: Contoh Lebih Lanjut - Penjumlahan Paralel dengan Sinkronisasi
Mari kita selesaikan masalah yang lebih realistis: menjumlahkan array angka yang sangat besar menggunakan beberapa worker. Kita akan menggunakan Atomics.wait()
dan Atomics.notify()
untuk sinkronisasi yang efisien.
Buffer bersama kita akan memiliki tiga bagian:
- Indeks 0: Bendera status (0 = sedang diproses, 1 = selesai).
- Indeks 1: Penghitung berapa banyak worker yang telah selesai.
- Indeks 2: Jumlah akhir.
main.js:
if (self.crossOriginIsolated) {
const NUM_WORKERS = 4;
const DATA_SIZE = 10_000_000;
// [status, workers_selesai, hasil]
// Kita menggunakan dua integer 32-bit untuk hasil agar tidak terjadi overflow untuk jumlah besar.
const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 integer
const sharedArray = new Int32Array(sharedBuffer);
// Buat beberapa data acak untuk diproses
const data = new Uint8Array(DATA_SIZE);
for (let i = 0; i < DATA_SIZE; i++) {
data[i] = Math.floor(Math.random() * 10);
}
const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);
for (let i = 0; i < NUM_WORKERS; i++) {
const worker = new Worker('sum_worker.js');
const start = i * chunkSize;
const end = Math.min(start + chunkSize, DATA_SIZE);
// Buat tampilan yang tidak dibagikan untuk potongan data worker
const dataChunk = data.subarray(start, end);
worker.postMessage({
sharedBuffer,
dataChunk // Ini disalin
});
}
console.log('Main thread sekarang menunggu para worker selesai...');
// Tunggu bendera status di indeks 0 menjadi 1
// Ini jauh lebih baik daripada loop while!
Atomics.wait(sharedArray, 0, 0); // Tunggu jika sharedArray[0] adalah 0
console.log('Main thread dibangunkan!');
const finalSum = Atomics.load(sharedArray, 2);
console.log(`Jumlah total paralel adalah: ${finalSum}`);
} else {
console.error('Halaman tidak terisolasi lintas-asal.');
}
sum_worker.js:
self.onmessage = ({ data }) => {
const { sharedBuffer, dataChunk } = data;
const sharedArray = new Int32Array(sharedBuffer);
// Hitung jumlah untuk potongan data worker ini
let localSum = 0;
for (let i = 0; i < dataChunk.length; i++) {
localSum += dataChunk[i];
}
// Secara atomik tambahkan jumlah lokal ke total bersama
Atomics.add(sharedArray, 2, localSum);
// Secara atomik naikkan penghitung 'worker selesai'
const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;
// Jika ini adalah worker terakhir yang selesai...
const NUM_WORKERS = 4; // Sebaiknya dilewatkan sebagai parameter di aplikasi nyata
if (finishedCount === NUM_WORKERS) {
console.log('Worker terakhir selesai. Memberi tahu main thread.');
// 1. Atur bendera status ke 1 (selesai)
Atomics.store(sharedArray, 0, 1);
// 2. Beri tahu main thread, yang sedang menunggu di indeks 0
Atomics.notify(sharedArray, 0, 1);
}
};
Kasus Penggunaan dan Aplikasi di Dunia Nyata
Di mana teknologi yang kuat namun kompleks ini benar-benar membuat perbedaan? Ia unggul dalam aplikasi yang memerlukan komputasi berat yang dapat diparalelkan pada dataset besar.
- WebAssembly (Wasm): Ini adalah kasus penggunaan utamanya. Bahasa seperti C++, Rust, dan Go memiliki dukungan matang untuk multithreading. Wasm memungkinkan pengembang untuk mengkompilasi aplikasi berkinerja tinggi dan multi-threaded yang sudah ada (seperti mesin game, perangkat lunak CAD, dan model ilmiah) untuk berjalan di browser, menggunakan
SharedArrayBuffer
sebagai mekanisme dasar untuk komunikasi thread. - Pemrosesan Data dalam Browser: Visualisasi data skala besar, inferensi model machine learning di sisi klien, dan simulasi ilmiah yang memproses data dalam jumlah besar dapat dipercepat secara signifikan.
- Penyuntingan Media: Menerapkan filter ke gambar beresolusi tinggi atau melakukan pemrosesan audio pada file suara dapat dipecah menjadi beberapa bagian dan diproses secara paralel oleh beberapa worker, memberikan umpan balik real-time kepada pengguna.
- Game Berkinerja Tinggi: Mesin game modern sangat bergantung pada multithreading untuk fisika, AI, dan pemuatan aset.
SharedArrayBuffer
memungkinkan pembuatan game berkualitas konsol yang berjalan sepenuhnya di browser.
Tantangan dan Pertimbangan Akhir
Meskipun SharedArrayBuffer
bersifat transformatif, ini bukanlah solusi untuk semua masalah. Ini adalah alat tingkat rendah yang memerlukan penanganan yang hati-hati.
- Kompleksitas: Pemrograman konkuren terkenal sulit. Melakukan debug pada kondisi balapan dan deadlock bisa sangat menantang. Anda harus berpikir secara berbeda tentang bagaimana status aplikasi Anda dikelola.
- Deadlock: Deadlock terjadi ketika dua atau lebih thread diblokir selamanya, masing-masing menunggu yang lain untuk melepaskan sumber daya. Ini bisa terjadi jika Anda mengimplementasikan mekanisme penguncian yang kompleks secara tidak benar.
- Beban Keamanan: Persyaratan isolasi lintas-asal adalah rintangan yang signifikan. Hal ini dapat merusak integrasi dengan layanan pihak ketiga, iklan, dan gateway pembayaran jika mereka tidak mendukung header CORS/CORP yang diperlukan.
- Bukan untuk Setiap Masalah: Untuk tugas latar belakang sederhana atau operasi I/O, model Web Worker tradisional dengan
postMessage()
seringkali lebih sederhana dan cukup. GunakanSharedArrayBuffer
hanya ketika Anda memiliki hambatan yang jelas dan terikat CPU yang melibatkan data dalam jumlah besar.
Kesimpulan
SharedArrayBuffer
, bersama dengan Atomics
dan Web Worker, mewakili pergeseran paradigma untuk pengembangan web. Ini menghancurkan batasan model single-threaded, mengundang kelas baru aplikasi yang kuat, berkinerja, dan kompleks ke dalam browser. Ini menempatkan platform web pada pijakan yang lebih setara dengan pengembangan aplikasi asli untuk tugas-tugas yang intensif secara komputasi.
Perjalanan menuju JavaScript konkuren sangat menantang, menuntut pendekatan yang ketat terhadap manajemen state, sinkronisasi, dan keamanan. Tetapi bagi para pengembang yang ingin mendorong batas dari apa yang mungkin di web—dari sintesis audio real-time hingga rendering 3D yang kompleks dan komputasi ilmiah—menguasai SharedArrayBuffer
bukan lagi sekadar pilihan; ini adalah keterampilan penting untuk membangun aplikasi web generasi berikutnya.